package Cammy::TimeLapse;
use strict;
use warnings;
# VERSION
# ABSTRACT: Generate a timelapse video given the configured parameters

=head1 SYNOPSIS

    use Cammy::TimeLapse;
    Cammy::TimeLapse->new->run;

=head1 DESCRIPTION

Cammy::TimeLapse is supposed to generate a video file showing a full day.

We omit images taken between sunset and sunrise, as those are too dark
to be useful. Also, we only sample the images, as using every image
will make the video too slow.

=cut

use Carp;
use Cammy::Util qw(http_client verify_config);
use Config::General qw(ParseConfig);
use DateTime;
use DateTime::Duration;
use DateTime::TimeZone;
use DateTime::Event::Sunrise;
use Date::Calc::Iterator;
use File::Basename qw(basename);
use File::Spec;
use File::Temp;
use IPC::Run3;
use List::MoreUtils qw(any);

=head1 METHODS

=head2 new

Constructs a new Cammy::TimeLapse object. Takes the following parameters:

=over 4

=item C<http> - inject an HTTP user agent (default is a L<HTTP::Tiny> object
constructed by L<Cammy::Util>)

=item C<config_file> - a filename of the config file to use (default is F<Cammy.conf>

=item C<debug> - whether to print debugging information

=item C<start_date> - a L<DateTime> object specifying when the video should start
(defaults to today)

=item C<end_date> - a L<DateTime> object specifying when the video should end
(defaults to today)

=item C<webcams> - an arrayref of webcam names to work on. Omit this to do
all of them.

=back

=cut

sub new {
    my $class = shift;
    my %args  = @_;
    my $self = {
        http        => $args{http} || $args{ua} || http_client(),
        config_file => $args{config_file} || 'Cammy.conf',
        debug       => $args{debug} || 0,
        start_date  => $args{start_date} || DateTime->today( time_zone => 'local' ),
        end_date    => $args{end_date},
        webcams     => $args{webcams},
        skip_dates  => $args{skip_dates},
        skip_weekends=>$args{skip_weekends},
        tz          => DateTime::TimeZone->new( name => 'local' ),
    };
    # use Data::Dumper; die Dumper $self->{start_date};
    $self->{end_date} = $self->{start_date} unless $self->{end_date};
    $self->{days} = _init_iterator( $self->{start_date}, $self->{end_date}, $self->{tz} );

    my %config = ParseConfig($self->{config_file});
    verify_config(\%config, $self->{debug});
    $self->{config} = \%config;

    return bless $self, $class;
}

sub _init_iterator {
    my $start = shift;
    my $end   = shift;
    my $tz    = shift;

    my @days;
    my $itr = Date::Calc::Iterator->new(
        from => [ $start->year, $start->month, $start->day ],
        to   => [ $end->year,   $end->month,   $end->day   ]
    );
    while (my $date = $itr->next){
        my ($year, $month, $day) = @$date;
        push @days, DateTime->new( year => $year, month => $month, day => $day, time_zone => $tz );
    }

    return \@days;
}

=head2 run

The only method you'll need to call after construction.

=cut

sub run {
    my $self = shift;

    # foreach webcam create a list of frames
    while (my ($cam, $conf) = each %{ $self->{config}->{webcams} }) {
        if ($self->{webcams}) {
            next unless any { $cam eq $_ } @{ $self->{webcams} };
        }

        # https://metacpan.org/module/DateTime::Event::Sunrise says:
        # Eastern longitude is entered as a positive number
        # Western longitude is entered as a negative number
        # Northern latitude is entered as a positive number
        # Southern latitude is entered as a negative number
        my $lat = do {
            if ($conf->{latitude} =~ m/^([\d.]+)([NS])$/) {
                $2 eq 'S' ? $1 * -1 : $1;
            }
            else {
                die "I can't undertand $conf->{latitude} as a latitude";
            }
        };
        my $long = do {
            if ($conf->{longitude} =~ m/^([\d.]+)([WE])$/) {
                $2 eq 'W' ? $1 * -1 : $1;
            }
            else {
                die "I can't understand $conf->{longitude} as a longitude";
            }
        };
        my $sunrise_calc = DateTime::Event::Sunrise->new( longitude => $long, latitude => $lat );

        my $frames = File::Temp->new();
        DAY:
        foreach my $day (@{ $self->{days} }) {
            if ($self->{skip_weekends} and $day->day_of_week == 6 || $day->day_of_week == 7) {
                warn "Skipping $day because it is a weekend" if $self->{debug};
                next DAY;
            }
            if ($self->{skip_dates}->contains( $day )) {
                warn "Skipping $day because it is in $self->{skip_dates}" if $self->{debug};
                next DAY;
            }

            # Calculate time span from which to take images
            my $daytime_span = $sunrise_calc->sunrise_sunset_span($day);
            my $min_time = $daytime_span->start
                + DateTime::Duration->new( hours => $conf->{sunrise_easement} );
            my $max_time = $daytime_span->end
                - DateTime::Duration->new( hours => $conf->{sunset_easement} );

            # Find all images for the current webcam's current day and sort them
            my @files = $self->_images_for_timelapse(
                day => $day,
                min => $min_time,
                max => $max_time,
                cam => $cam,
            );
            print $frames join("\n", @files), "\n" if @files;
        }
        close $frames; # doesn't unlink the tmpfile yet, but flushes the buffers
        if ($self->{debug}) {
            require File::Copy;
            File::Copy::copy( $frames->filename, 'frames.list' )
                or warn "Couldn't copy @{[ $frames->filename ]} to frames.list: $!";
            warn 'Copied the list of frames to frames.list';
        }
        $self->generate_video($cam, $frames->filename);
    }
}

=head2 generate_video

Create the various video formats configured.

=cut

sub generate_video {
    my $self = shift;
    my $cam  = shift;
    my $file = shift;
    my $conf = $self->{config}->{webcams}->{$cam};
    local $self->{size} = $conf->{timelapse_size};

    my $timelapse_filename_base = File::Spec->catfile(
        $self->{config}->{out_dir},
        $cam,
        ( $self->{end_date}->mdy eq $self->{start_date}->mdy
            ? ($self->{start_date}->mdy(''), 'timelapse')
            : ($self->{start_date}->mdy . '_to_' . $self->{end_date}->mdy . '_timelapse')
        )
    );

    my %video = (master => $file); # master needs the frames filename instead
    foreach my $output (qw(master webm ogv h264)) {
        my $s = $self->can( "create_${output}_video" );
        unless ($s and ref $s eq 'CODE') {
            warn "Don't know how to make $output videos";
            next;
        }

        $video{$output} = $self->$s( $timelapse_filename_base, $video{master} );
    }
    unlink delete $video{master}; # unneeded after derivative formats created

    while (my ($format, $file) = each %video) {
        print "Your $format file is '$file'\n";
    }
}

=head2 create_master_video

Creates a video from the still frames selected from the webcam.
This is a temporary file that is used to create web-friendly
derivative formats. Uses C<mencoder>, not C<avconv>.

=cut

sub create_master_video {
    my $self     = shift;
    my $filename = shift;
    my $frames   = shift;
    $filename   .= '.avi';

    my @mencoder_cmd = (
        mencoder    => 'mf://@'.$frames,
        -o          => $filename,
        -mf         => 'type=jpg',
        -ovc        => 'copy',
        -oac        => 'copy',
        -lavcopts   => 'vcodec=mpeg4:mbd=2:trell',
    );

    if ($self->{debug}) {
        warn "Running: @mencoder_cmd\n";
        run3 \@mencoder_cmd; # inherit stdin/out/err from parent - we'll see the output in that case
    }
    else {
        run3 \@mencoder_cmd, \undef, \undef, \undef; # redirect stdin/out/err to /dev/null
    }

    return $filename;
}

=head2 create_webm_video

Creates a WebM video using the VP8 codec. Requires C<avconv> to be
compiled with libvp8 support.

=cut

sub create_webm_video {
    my $self     = shift;
    my $filename = shift;
    my $master   = shift;
    $filename   .= '.webm';

    my @webm_cmd = ( 'avconv', '-i', $master, '-qscale', 10, '-s', $self->{size}, $filename );
    if ($self->{debug}) {
        warn "Running: @webm_cmd\n";
        run3 \@webm_cmd;
    }
    else {
        run3 \@webm_cmd, \undef, \undef, \undef;
    }

    return $filename;
}

=head2 create_ogv_video

Creates video encoded with the Theora codec. Requires C<avconv> to be
compiled with libtheora support.

=cut

sub create_ogv_video {
    my $self     = shift;
    my $filename = shift;
    my $master   = shift;
    $filename   .= '.ogv';

    my @ogv_cmd = ( 'avconv', '-i', $master, '-qscale', 10, '-s', $self->{size}, $filename );
    if ($self->{debug}) {
        warn "Running: @ogv_cmd\n";
        run3 \@ogv_cmd;
    }
    else {
        run3 \@ogv_cmd, \undef, \undef, \undef;
    }

    return $filename;
}

=head2 create_h264_video

Uses C<avconv> to create a video file encoded with the H.264 codec.
C<avconv> must be compiled with libx264 support.

=cut

sub create_h264_video {
    my $self     = shift;
    my $filename = shift;
    my $master   = shift;
    $filename   .= '.mp4';

    my @h264_cmd = ( 'avconv', '-i', $master, '-c:v', 'libx264', '-qscale', 10, '-s', $self->{size}, $filename );
    if ($self->{debug}) {
        warn "Running command: @h264_cmd\n";
        run3 \@h264_cmd;
    }
    else {
        run3 \@h264_cmd, \undef, \undef, \undef;
    }

    return $filename;
}

sub _images_for_timelapse {
    my $self     = shift;
    my %opts     = @_;
    my $day      = $opts{day}->mdy('');
    my $cam      = $opts{cam};
    my $min_time = $opts{min}->epoch;
    my $max_time = $opts{max}->epoch;
    my @image_list;

    my $dir = File::Spec->catdir( $self->{config}->{out_dir}, $cam, $day );
    if (-d $dir) {
        my @images =
            map  { $_->[0] }
            sort { $a->[1] <=> $b->[1] }
            map  { [ $_, number_from_filename($_) ] }
            glob( File::Spec->catfile($dir, '*.jpg') );

        # Old school for-loop for array iteration. Feels ugly in perl.
        IMAGE:
        for(my $i = 0; $i < scalar(@images); $i += $self->{config}->{interval}) {
            my $image = $images[$i];
            my $image_date;
            if ( basename($image) =~ m/^(\d+\.\d+)\.jpg$/ ) {
                $image_date = $1;
            }
            else {
                next IMAGE;
            }

            push @image_list, $image
                if $min_time <= $image_date && $max_time >= $image_date;

            last IMAGE if $image_date > $max_time; # The list is sorted, so just stop here
        }
    }

    return @image_list;
}

=head2 number_from_filename

Extracts the nubmer part of a full pathname.

=cut

sub number_from_filename {
    my $filename = shift;

    if ( basename($filename) =~ m/^(\d+\.\d+)\.jpg$/ ) {
        return $1;
    }
}

1;
